package com.bensonbasement.snorflux.server.world;

import java.io.Serializable;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.bensonbasement.snorflux.memory.Memory;
import com.bensonbasement.snorflux.memory.WorldElementInfo;
import com.bensonbasement.snorflux.messages.fromserver.reports.Report;
import com.bensonbasement.snorflux.server.events.Event;

public class BrainTeam implements IViewer, Serializable {
	
	private final List<Brain> brains;
	private HashSet<Space> visibleSpaces;
	private final HashSet<Long> currentlySeenWorldElementIDs;
	
	private Memory memory;
	
	public BrainTeam() {
		this.brains = new LinkedList<Brain>();
		this.visibleSpaces = new HashSet<Space>();
		this.currentlySeenWorldElementIDs = new HashSet<Long>();
		this.memory = new Memory();
	}
	
	/**
	 * Should ONLY be called from the Brain class.
	 * @param brain
	 */
	public void addBrain(Brain brain) {
		this.brains.add(brain);
	}
	
	/**
	 * Should ONLY be called from the Brain class.
	 * @param brain
	 */
	public void removeBrain(Brain brain) {
		this.brains.remove(brain);
	}
	
	public void actorVisibilityUpdated() {
		// Recalculate our total visibility.
		HashSet<Space> oldVisibleSpaces = this.visibleSpaces;
		HashSet<Space> newVisibleSpaces = this.calculateVisibility();
		this.visibleSpaces = newVisibleSpaces;
		
		// Calculate the change in visibility (spaces added or removed)
		HashSet<Space> newlyVisibleSpaces = (HashSet<Space>) newVisibleSpaces.clone();
		newlyVisibleSpaces.removeAll(oldVisibleSpaces);
		HashSet<Space> newlyNonVisibleSpaces = (HashSet<Space>) oldVisibleSpaces.clone();
		newlyNonVisibleSpaces.removeAll(newVisibleSpaces);
		
		// Use these to send information to the brain.
		HashSet<Long> newlyVisibleSpaceIDs = new HashSet<Long>();
		HashSet<Long> newlyNonVisibleSpaceIDs = new HashSet<Long>();
		
		for (Space space : newlyVisibleSpaces) {
			newlyVisibleSpaceIDs.add(space.getWorldElementID());
			// Refresh info
			this.updateInfoTree(space);
		}
		for (Space space : newlyNonVisibleSpaces) {
			newlyNonVisibleSpaceIDs.add(space.getWorldElementID());
			for (WorldElement element : space.getAllWorldElements()) {
				long worldElementID = element.getWorldElementID();
				if (this.currentlySeenWorldElementIDs.contains(worldElementID)) {
					this.currentlySeenWorldElementIDs.remove(worldElementID);
				}
			}
		}
		this.distributeVisibilityUpdate(newlyVisibleSpaceIDs,newlyNonVisibleSpaceIDs);
	}
	
	private HashSet<Space> calculateVisibility() {
		HashSet<Space> newVisibleSpaces = new HashSet<Space>();
		for (Brain brain : this.brains) {
			for (Actor actor : brain.getControlledActors()) {
				newVisibleSpaces.addAll(actor.getVisibleSpaces());
			}
		}
		return newVisibleSpaces;
	}
	
	@Override
	public boolean canSeeSpace(Space space) {
		return (this.visibleSpaces.contains(space));
	}
	
	@Override
	public boolean canSeeInLocation(ILocation location) {
		if (location instanceof Space) {
			return this.canSeeSpace((Space)location);
		}
		else {
			HashSet<Actor> allSeeingActors = this.getAllSeeingActors();
			return (allSeeingActors.contains(location)); // Will return true if the location is an actor in this brainTeam.
		}
	}
	
	private HashSet<Actor> getAllSeeingActors() {
		HashSet<Actor> retVal = new HashSet<Actor>();
		for (Brain brain : this.brains) {
			retVal.addAll(brain.getControlledActors());
		}
		return retVal;
	}
	
	/**
	 * Called from Level.forceInfoRefresh when a worldElement
	 * has changed somehow.
	 * @param element
	 */
	public void updateInfo(WorldElement element) {
		if (this.currentlySeenWorldElementIDs.contains(element.getAllWorldElements())) {
			// If the element was already visible before, then we only
			// need to update IT, not its children.
			this.updateOneInfo(element);
		}
		else {
			// If we're encountering the element for the first time or
			// it left our vision and has returned, we need to update
			// everything.
			this.updateInfoTree(element);
		}
	}
	
	private void updateInfoTree(WorldElement element) {
		for (WorldElement otherElement : element.getAllWorldElements()) {
			this.updateOneInfo(otherElement);
		}
	}
	
	private void updateOneInfo(WorldElement element) {
		WorldElementInfo info = element.render(this);
		if (info == null) {
			if (this.currentlySeenWorldElementIDs.contains(element.getWorldElementID())) {
				this.deleteInfoTree(element);
			}
		}
		else {
			this.currentlySeenWorldElementIDs.add(element.getWorldElementID());
			this.distributeInfo(info);
		}
	}
	
	private void deleteInfoTree(WorldElement element) {
		for (WorldElement otherWorldElement : element.getAllWorldElements()) {
			long worldElementID = otherWorldElement.getWorldElementID();
			if (this.currentlySeenWorldElementIDs.contains(worldElementID)) {
				this.currentlySeenWorldElementIDs.remove(worldElementID);
				this.distributeInfoDeletion(worldElementID);
			}
		}
	}
	
	private void distributeInfo(WorldElementInfo info) {
		for (Brain brain : this.brains) {
			brain.receiveInfo(info);
		}
		memory.updateInfo(info);
	}
	
	private void distributeInfoDeletion(long worldElementID) {
		for (Brain brain : this.brains) {
			brain.deleteInfo(worldElementID);
		}
		memory.forgetEntity(worldElementID);
	}
	
	private void distributeVisibilityUpdate(Set<Long> newlyVisibleSpaceIDs, Set<Long> newlyNonVisibleSpaceIDs) {
		for (Brain brain : this.brains) {
			brain.updateVisibility(newlyVisibleSpaceIDs, newlyNonVisibleSpaceIDs);
		}
		memory.updateVisibility(newlyVisibleSpaceIDs, newlyNonVisibleSpaceIDs);
	}
	
	public void receiveEvent(Event event) {
		Report report = event.render(this);
		if (report != null) {
			for (Brain brain : this.brains) {
				brain.receiveReport(report);
			}
		}
		// TODO: If memories ever do anything with events, this will need to be changed.
	}
	
	public Memory getMemory() {
		return memory;
	}
}
